Skip to content

feat: intent-swap extension — solve signed Fusion orders for same-block inclusion#1

Open
kayibal wants to merge 6 commits into
mainfrom
feat/intent-swap
Open

feat: intent-swap extension — solve signed Fusion orders for same-block inclusion#1
kayibal wants to merge 6 commits into
mainfrom
feat/intent-swap

Conversation

@kayibal

@kayibal kayibal commented Jul 21, 2026

Copy link
Copy Markdown

Summary

Adds an in-process intent-swap service to the Base builder. Signed 1inch Fusion orders arrive over a new intent_submitOrder RPC, are solved on arrival against committed AMM state overlaid with the transactions the builder has executed so far in the in-progress block, and the signed settlement is inserted into the builder's txpool for same-block inclusion. Settlement uses the existing Fusion resolver contracts; the solver is the backrunner crate (Tycho state + Fynd routing), consumed as a git dependency.

This is a proof of concept, intended to derive concrete requirements for the feature — see Limitations.

Reading guide (commits in order)

  1. feat(builder-core): stream block-build lifecycle events — an optional, non-blocking build-event stream from the flashblocks payload builder (BuildEventEmitter, held like ws_pub). No effect when no consumer is attached. Isolated to builder-core.
  2. feat(intent-swap): add crate with intent_submitOrder RPC and config — the new base-intent-swap crate: RPC ingress, validation, config, and the engine dependency pin.
  3. feat(intent-swap): solve submitted orders against pending state — the engine: solve-on-arrival, pending-state overlay, pool-derived nonce, typed settlement, inclusion telemetry.
  4. feat(intent-swap): node extension, CLI flags, and builder wiring — the BaseNodeExtension, --builder.intent-swap.* flags (off by default), bin wiring.
  5. docs(intent-swap): usage and Base-shadow runbook.

Notes for reviewers

  • Disabled by default: with --builder.intent-swap.enabled off, the builder is unchanged — no channel, no events, no extension.
  • RPC validation checks order shape and the Dutch-auction window; it does not recover the maker's EIP-712 signature (the on-chain LOP validates it). An optional pre-flight (verify_onchain_taking, off by default) can reject unfillable orders before signing.
  • The EOA signing key is redacted from Debug output.
  • Engine startup failure panics (reth critical-task fail-fast → node exits) rather than degrading silently.

Testing

  • base-intent-swap: RPC validation paths, uuid derivation, sign→recover roundtrip, config redaction.
  • base-builder-core: driver test asserting the build-event sequence and single-payload-id correlation.
  • cargo check + clippy -D warnings + fmt clean on builder-core, intent-swap, cli, and the builder bin.

Build notes

  • Cargo.lock pins tycho-common/-simulation/-execution at 0.305.1 deliberately: fynd-core 0.81.1 does not compile against the 0.339.x line. Do not regenerate the lockfile.

Limitations / out of scope (PoC)

  • Each order is solved once, when it is submitted; it is not re-solved against fresher state as later transactions land, so an order that is not yet fillable at submission can be missed. Re-solving on new build events (per included transaction, or per flashblock) is a natural extension.
  • Consuming the tycho/fynd stack in-process pulls a second, older generation of ~35 crates into the builder graph, including a second revm. It compiles and links (the engine↔builder boundary is plain data, so the two revm versions never meet at a type boundary), but cargo deny check bans flags the duplicates. Resolving this means either an out-of-process/sidecar deployment or upgrading the tycho stack to Base's revm generation — deferred.
  • Pending-state overlay covers the protocols the backrunner supports (Uniswap v2/v3/v4).
  • Shadow-run validation only; the builder has no built-in non-proposing mode (the runbook covers op-node / forkchoice replay).

🤖 Generated with Claude Code

@socket-security

socket-security Bot commented Jul 21, 2026

Copy link
Copy Markdown

@kayibal
kayibal force-pushed the feat/intent-swap branch from 70c2836 to a053c79 Compare July 22, 2026 10:48
kayibal and others added 5 commits July 22, 2026 12:50
Add an optional, non-blocking stream of block-build lifecycle events so
in-process services can observe block construction as it happens.

BuildEventEmitter owns an optional mpsc sender and is held as
Arc<BuildEventEmitter> on the payload builder and its context, built in
service.rs and mirroring the existing ws_pub / rejected_tx_sender wiring.
Emission uses try_send and never blocks building: a full channel drops the
event (backpressure), and a closed channel — the consumer having gone away
— warns once per emitter rather than on every subsequent transaction.

The payload builder emits four events per job, correlated by payload id:
IterationStart, one TxExecuted per committed transaction (carrying its
receipt logs), and a terminal IterationComplete or IterationAborted. When
no consumer is attached the emitter is a no-op and BuilderConfig carries
only the Option<BuildEventSender> wiring handle as data.

A driver test asserts the full event sequence and single-payload-id
correlation for a built block.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce the base-intent-swap crate: an in-process service that accepts
signed 1inch Fusion orders and (in later commits) settles them into the
current block.

This commit adds the RPC ingress and configuration. intent_submitOrder
validates order shape and the Dutch-auction window and enqueues accepted
orders; it deliberately does not recover the maker's EIP-712 signature —
the on-chain LOP validates it, and an optional pre-flight
(IntentSwapConfig::verify_onchain_taking, default off) can reject
unfillable orders before signing. Distinct error codes cover malformed
signature, expired auction, invalid order, a full intake queue, and an
unavailable engine. The EOA signing key is redacted from Debug.

The solver engine is the backrunner crate, consumed as a git dependency
pinned by rev to builder-integration main. The tycho / fynd stack it pulls
in pins tycho-common/-simulation/-execution at 0.305.1 in Cargo.lock;
these are load-bearing, as fynd-core 0.81.1 does not compile against the
newer 0.339.x line, so the lockfile must not be regenerated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the engine task: it consumes the build-event stream to track the
in-progress block and, on each submitted order, solves it against
committed state overlaid with that block's executed transactions, signs
the resulting settlement, and inserts it into the txpool for same-block
inclusion.

The iteration state machine attributes TxExecuted events by payload id, so
a stale event from a superseded payload job cannot pollute the current
overlay. At block seal or abort the accumulated transactions are cleared:
the sealed block's transactions become committed state and must not be
double-counted, and an aborted job's transactions never landed — so orders
arriving between blocks solve against last committed state.

The settlement nonce is read from the pool's highest transaction for the
signer per candidate (chaining locally within a candidate) rather than a
counter that could permanently desync from pool state. The settlement
stays a typed Recovered<BaseTransactionSigned> from signing through
insertion, recovering the signer from the transaction itself instead of
trusting a separately-threaded address. Inclusion is reported by observing
the builder's own settlement transactions in the event stream, distinct
from pool acceptance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire the intent-swap service into the builder. IntentSwapExtension
registers the intent_ RPC namespace and, on node start, spawns the engine
task with a handle to the txpool. If the engine fails to start it panics
rather than returning: reth's critical-task fail-fast only triggers on
panic, and a silently dead engine behind a running builder is worse than a
restart.

A --builder.intent-swap.* CLI group (disabled by default) configures the
service; the flag parsing lives in the builder binary so the cli crate
gains no new dependencies. With the extension disabled the builder behaves
exactly as before — no channel is created and no events are emitted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Document running the service against Base in non-proposing (shadow) mode —
the builder has no built-in shadow flag, so the runbook covers driving it
via op-node / forkchoice replay — and record the load-bearing Cargo.lock
pins (fynd-core 0.81.1's open tycho ranges do not build against 0.339.x).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kayibal
kayibal force-pushed the feat/intent-swap branch from a053c79 to 2c18f8d Compare July 22, 2026 11:53
The order_id was accepted as any non-empty string even though it claims
to be the order's EIP-712 digest, so spoofed or garbage ids flowed into
logs and RPC responses unchecked. Reject anything that is not 0x plus
64 hex chars with INVALID_ORDER.

This is a format check only. Deriving the digest server-side and
ignoring the client-supplied value entirely belongs to the maker-sig
verification follow-up, which already computes it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant